require'zdc.idutil'
require'zdc.sandbox'
local fsutil=require'zdc.ds.util'
local isparent=zdc.idutil.IsParent
local IsRoot=zdc.idutil.IsRoot
local PreRemove=zdc.idutil.PreRemove
local conf_do = zdc.sandbox.conf_do
module(..., package.seeall)

FileDB = {}
FileDB_mt ={__metatable = {}, __index = FileDB}

function _getdbwherefun(where)
	if not where or #where==0 then return end
	local f=assert(loadstring('return ' .. where))
	local env={}
	local _dbfun={
		ik='ik',
		ts='ts',
		AND=function(...)
			local cnt=select('#',...)
			for i=1,cnt do
				if not select(i,...) then return false end
			end
			return cnt>0
		end,
		OR=function(...)
			for i=1,select('#',...) do
				if select(i,...) then return true end
			end
			return false
		end,
		GT=function(k,a)
			if k=='ik' then return env._ik>a end
			if k=='ts' then return env._ts>a end
		end,
		LT=function(k,a)
			if k=='ik' then return env._ik<a end
			if k=='ts' then return env._ts<a end
		end,
		GE=function(k,a)
			if k=='ik' then return env._ik>=a end
			if k=='ts' then return env._ts>=a end
		end,
		LE=function(k,a)
			if k=='ik' then return env._ik<=a end
			if k=='ts' then return env._ts<=a end
		end,
		EQ=function(k,a)
			if k=='ik' then return env._ik==a end
			if k=='ts' then return env._ts==a end
		end,
	}
	local _wheremeta= {__index=_dbfun}
	setmetatable(env,_wheremeta)
	setfenv(f,env)
	return f,env
end

function new(params)
	local obj = {dir=params[1],pre=params[2] or ''}
	if #obj.pre>0 then
		obj.pre_=obj.pre..'/'
	else
		obj.pre_=''
	end
	setmetatable(obj, FileDB_mt)
	obj:init()
	return obj
end

function FileDB:transdo(fn)
	return fn()
end

function FileDB:deflate_node(a,b,c)
	return zdc.deflate.deflate_node(a,b,c)
end

function FileDB:init()
	self.store={}
	local function attrdir (path,prem)
		for file in lfs.dir(path) do
			if file:sub(1,1)~='.' then
				local f = path..'/'..file
				local attr = lfs.attributes (f)
				if attr.mode == "directory" then
					attrdir (f,prem .. file .. '/')
				else
					local k = string.match(file, "^(.+)%.zmd$")
					if k then
						self.store[fsutil.fs_unescape_id(prem .. k)] = 1
						print("scan file node",prem .. k)
					end
				end
			end
		end
	end
	attrdir (self.dir,self.pre_)
end

function FileDB:reset_cache()
	self:init()
end
function FileDB:node_exists(id)
	return self.store[id] ~= nil
	--local path = self.dir.."/"..fsutil.fs_escape_id_but_keep_slash(id)..'.zmd'
	--return fsutil.file_exist(path,id)
end

function FileDB:get_node(id)
	if not self:node_exists(id) then
		return nil
	end
	
	local deltaid=PreRemove(id,self.pre_)
	assert(deltaid)
	
	local path = self.dir.."/"..fsutil.fs_escape_id_but_keep_slash(deltaid)..'.zmd'
	local vvv,err = fsutil.read_file_if_exists(path)
	if vvv then
		vvv,err=conf_do(vvv,nil,self.logger)
		if vvv and not vvv.data then
			print('WARN:',id,path,'not data define, use {} instead')
			vvv.data={}
		end
	end
	return vvv
end

function FileDB:save_node(id,data,ik,ts)
	self.store[id] = 1
	local deltaid=PreRemove(id,self.pre_)
	assert(deltaid)

	local path = self.dir.."/"..fsutil.fs_escape_id_but_keep_slash(deltaid)..'.zmd'
	-- create a directory if necessary
	fsutil.suredir(path)
	local data={data=data,ik=ik or 0,ts=ts or os.time()}
	fsutil.write_file(path, self:deflate_node(data), id)
end

function FileDB:get_child_nodes(parentid)
	local nodes = {}
	for id,v in pairs(self.store) do
		if isparent(parentid,id) then
			nodes[id]=self:get_node(id)
		end
	end
	return nodes
end
function FileDB:get_child_nodes_ex(parentid,where,sort,limit)
	local nodes = {}
	local fw,fenv=_getdbwherefun(where)
	for id,v in pairs(self.store) do
		if isparent(parentid,id) then
			if fw then
				local vn=self:get_node(id)
				fenv._ik=vn.ik
				fenv._ts=vn.ts
				if fw() then
					nodes[id]=vn
				end
			else
				nodes[id]=self:get_node(id)
			end
		end
	end
	local st={}
	for id,v in pairs(nodes) do
		table.insert(st,id)
	end
	if sort or limit then
		if sort then
			table.sort(st,function(a,b)
				for k,op in string.gmatch(sort,"(%a+)(%-?)") do
					local ika,ikb
					if k=='ik' then
						ika,ikb=nodes[a].ik,nodes[b].ik
					elseif k=='ts' then
						ika,ikb=nodes[a].ts,nodes[b].ts
					elseif k=='id' then
						ika,ikb=nodes[a].id,nodes[b].id
					else
						error('unknow sort:' .. sort)
					end
					if ika~=ikb then
						if op=='-' then
							return ika>ikb
						else
							return ika<ikb
						end
					end
				end
				return false
			end)
		end
		if limit then
			for i=limit+1,#st do
				nodes[st[i]]=nil
			end
		end
	end
	return nodes,st
end

function FileDB:get_nodes_by_prefix(prefix)
	local nodes = {}
	prefix = prefix or ""
	local preflen = #prefix
	for id,v in pairs(self.store) do
		if string.sub(id,1,preflen)==prefix then
			nodes[id]=self:get_node(id)
		end
	end
	return nodes
end

function FileDB:get_nodes_by_prefix_ex(prefix,where,sort,limit)
	prefix = prefix or ""
	local nodes = {}
	local fw,fenv=_getdbwherefun(where)
	local prelen=#prefix
	for id,v in pairs(self.store) do
		if id:sub(1,prelen)==prefix then
			if fw then
				local vn=self:get_node(id)
				fenv._ik=vn.ik
				fenv._ts=vn.ts
				if fw() then
					nodes[id]=vn
				end
			else
				nodes[id]=self:get_node(id)
			end
		end
	end
	local st={}
	for id,v in pairs(nodes) do
		table.insert(st,id)
	end
	if sort or limit then
		if sort then
			table.sort(st,function(a,b)
				for k,op in string.gmatch(sort,"(%a+)(%-?)") do
					local ika,ikb
					if k=='ik' then
						ika,ikb=nodes[a].ik,nodes[b].ik
					elseif k=='ts' then
						ika,ikb=nodes[a].ts,nodes[b].ts
					elseif k=='id' then
						ika,ikb=nodes[a].id,nodes[b].id
					else
						error('unknow sort:' .. sort)
					end
					if ika~=ikb then
						if op=='-' then
							return ika>ikb
						else
							return ika<ikb
						end
					end
				end
				return false
			end)
		end
		if limit then
			for i=limit+1,#st do
				nodes[st[i]]=nil
			end
		end
	end
	return nodes,st
end

function FileDB:get_ids_by_prefix(prefix)
	local ids = {}
	prefix = prefix or ""
	local preflen = prefix:len()
	for id,_ in pairs(self.store) do
		if string.sub(id,1,preflen)==prefix then
			table.insert(ids,id)
		end
	end
	return ids
end
function FileDB:get_ids_by_prefix_ex(prefix,where,sort,limit)
	local nodes = {}
	local fw,fenv=_getdbwherefun(where)
	local prelen=#prefix
	for id,v in pairs(self.store) do
		if id:sub(1,prelen)==prefix then
			if fw then
				local vn=self:get_node(id)
				fenv._ik=vn.ik
				fenv._ts=vn.ts
				if fw() then
					nodes[id]=vn
				end
			else
				nodes[id]=self:get_node(id)
			end
		end
	end
	local st={}
	for id,v in pairs(nodes) do
		table.insert(st,id)
	end

	if sort or limit then
		if sort then
			table.sort(st,function(a,b)
				for k,op in string.gmatch(sort,"(%a+)(%-?)") do
					local ika,ikb
					if k=='ik' then
						ika,ikb=nodes[a].ik,nodes[b].ik
					elseif k=='ts' then
						ika,ikb=nodes[a].ts,nodes[b].ts
					elseif k=='id' then
						ika,ikb=nodes[a].id,nodes[b].id
					else
						error('unknow sort:' .. sort)
					end
					if ika~=ikb then
						if op=='-' then
							return ika>ikb
						else
							return ika<ikb
						end
					end
				end
				return false
			end)
		end
		if limit then
			for i=limit+1,#st do
				st[i]=nil
			end
		end
	end
	return st
end

function FileDB:get_child_ids(parentid)
	local nodes = {}
	for id,v in pairs(self.store) do
		if isparent(parentid,id) then
			table.insert(nodes,id)
		end
	end
	return nodes
end
function FileDB:get_child_ids_ex(parentid,where,sort,limit)
	local nodes = {}
	local fw,fenv=_getdbwherefun(where)
	for id,v in pairs(self.store) do
		if isparent(parentid,id) then
			if fw then
				local vn=self:get_node(id)
				fenv._ik=vn.ik
				fenv._ts=vn.ts
				if fw() then
					nodes[id]=vn
				end
			else
				nodes[id]=self:get_node(id)
			end
		end
	end
	local st={}
	for id,v in pairs(nodes) do
		table.insert(st,id)
	end
	if sort or limit then
		if sort then
			table.sort(st,function(a,b)
				for k,op in string.gmatch(sort,"(%a+)(%-?)") do
					local ika,ikb
					if k=='ik' then
						ika,ikb=nodes[a].ik,nodes[b].ik
					elseif k=='ts' then
						ika,ikb=nodes[a].ts,nodes[b].ts
					elseif k=='id' then
						ika,ikb=nodes[a].id,nodes[b].id
					else
						error('unknow sort:' .. sort)
					end
					if ika~=ikb then
						if op=='-' then
							return ika>ikb
						else
							return ika<ikb
						end
					end
				end
				return false
			end)
		end
		if limit then
			for i=limit+1,#st do
				st[i]=nil
			end
		end
	end
	return st
end

function FileDB:get_cnt_by_prefix(prefix)
	local cnt = 0
	prefix = prefix or ""
	local preflen = prefix:len()
	for id,_ in pairs(self.store) do
		if string.sub(id,1,preflen)==prefix then
			cnt=cnt+1
		end
	end
	return cnt
end
function FileDB:get_cnt_by_prefix_ex(prefix,where)
	local cnt=0
	local fw,fenv=_getdbwherefun(where)
	local prelen=#prefix
	for id,v in pairs(self.store) do
		if id:sub(1,prelen)==prefix then
			if fw then
				local vn=self:get_node(id)
				fenv._ik=vn.ik
				fenv._ts=vn.ts
				if fw() then
					cnt=cnt+1
				end
			else
				cnt=cnt+1
			end
		end
	end
	return cnt
end
function FileDB:get_child_cnt(parentid)
	local cnt=0
	for id,v in pairs(self.store) do
		if isparent(parentid,id) then
			cnt=cnt+1
		end
	end
	return cnt
end
function FileDB:get_child_cnt_ex(parentid,where)
	local cnt=0
	local fw,fenv=_getdbwherefun(where)
	for id,v in pairs(self.store) do
		if isparent(parentid,id) then
			if fw then
				local vn=self:get_node(id)
				fenv._ik=vn.ik
				fenv._ts=vn.ts
				if fw() then
					cnt=cnt+1
				end
			else
				cnt=cnt+1
			end
		end
	end
	return cnt
end

function FileDB:del_node(id,deep)
	self.store[id]=nil
	local deltaid=PreRemove(id,self.pre_)
	assert(deltaid)
	
	local ppdir=self.dir.."/"..fsutil.fs_escape_id_but_keep_slash(deltaid)
	local path = ppdir..'.zmd'
	if fsutil.file_exist(path,id) then
		assert(os.remove(path))
	end
	if deep then self:del_sub_nodes(id) end
end

function FileDB:del_sub_nodes(parentid)
	local sid=self:get_ids_by_prefix(parentid .. '/')
	for i=1,#sid do
		self.store[sid[i]]=nil
	end
		
	if IsRoot(parentid,self.pre) then
		parentid=''
		pcall(fsutil.cleardir,ppdir)
	else
		parentid=PreRemove(parentid,self.pre_)
		local ppdir=self.dir.."/"..fsutil.fs_escape_id_but_keep_slash(parentid)
		pcall(fsutil.rmdir,ppdir)
	end
end

function FileDB:get_label(id)
	assert(id)
	local deltaid=PreRemove(id,self.pre_)
	assert(deltaid)
	
	local path = self.dir.."/"..fsutil.fs_escape_id_but_keep_slash(deltaid)..'.label'
	local vvv,err = fsutil.read_file_if_exists(path)
	if vvv then
		local row
		row,err=conf_do(vvv,nil,self.logger)
		if row then
			return row.idx,row.sum,row.timestamp,row.usr
		end
	end
	return 0,'',''
end

function FileDB:set_label(id, idx,sum,timestamp,usr)
	assert (id)
	assert(idx)
	assert(sum)
	--assert(timestamp)
	--assert(usr)
	
	local deltaid=PreRemove(id,self.pre_)
	assert(deltaid)

	local path = self.dir.."/"..fsutil.fs_escape_id_but_keep_slash(deltaid)..'.label'
	-- create a directory if necessary
	fsutil.suredir(path)
	local data={idx=idx,sum=sum,timestamp=timestamp,usr=usr}
	fsutil.write_file(path, self:deflate_node(data), id)
end

function FileDB:get_nodes_sum(rootid, sumfun)
	return sumfun(nil,rootid,'')
end